/*
Copyright (c) 2007, EffectiveUI, Inc.

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    * Neither the name of the EffectiveUI nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@ignore
*/
package com.effectiveui.frameworks.jest {

	import com.effectiveui.frameworks.jest.events.GestureRecognizedEvent;
	import com.effectiveui.frameworks.jest.events.UnrecognizedGestureEvent;
	
	import flash.display.Stage;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.KeyboardEvent;
	import flash.events.MouseEvent;
	import flash.events.TimerEvent;
	import flash.geom.Point;
	import flash.ui.Keyboard;
	import flash.utils.Timer;
	
	import mx.containers.Canvas;
	import mx.controls.Alert;
	import mx.core.IMXMLObject;
	import mx.core.UIComponent;
	import mx.events.FlexEvent;
	import mx.managers.ISystemManager;
	
   /** 
    *
    * <p>
    * Jest is the class that recognizes mouse gestures. Gestures are 
    * configured in the concrete JestController you implement, but 
    * an instance of Jest needs to be placed somewhere "high up", for instance 
    * on your application's Application, e.g.:
    * <pre>
    * &lt;mx:Application xmlns:jest="com.effectiveui.frameworks.jest.*"... &gt;
    * ...
    * 	&lt;jest:Jest ... / &gt;
    * </pre>
    * </p>
    * 
    * @see com.effectiveui.frameworks.jest.controller.JestController
    * @author tony
    * 
    */
	public class Jest extends EventDispatcher implements IMXMLObject {
		
		[Event(name='gestureRecognized', type="com.effectiveui.frameworks.jest.events.GestureRecognizedEvent")]
		[Event(name='unrecognizedGesture', type="com.effectiveui.frameworks.jest.events.UnecognizedGestureEvent")]
		
		private var sampleResolution:int		= 30;
		private var secondaryAxisTolerance:int	= 10;
		private var inactivityLimit:int			= 120; // samples until gesture is cancelled
		private var pathAnalysisResolution:int	= 2;
		
		private var _expectControlKey:Boolean	= true;
		private var _expectAltKey:Boolean		= false;
		private var _expectShiftKey:Boolean		= false;
		private var _expectSpaceKey:Boolean		= false;
		//TODO: move these to a style definition
		private var _lineThickness:int			= 1;
		private var _lineColor:uint				= 0;
		
		private var timer:Timer;
		private var stage:Stage;
		private var samples:Array;
		private var gestures:Array;
		private var mouseDown:Boolean;
		private var mouseOutOfStage:Boolean;
		private var spaceDown:Boolean;
		private var sm:ISystemManager;
		private var parentView:UIComponent;
		private var drawingCanvas:Canvas;
		private var currentAnchor:Point;
		private var lastSample:Point;
		private var lastDirection:String;
		private var inactiveSamples:int;
		private var originalMouseEvent:MouseEvent;
		
		public function Jest() {
			timer = new Timer(sampleResolution);
			timer.addEventListener(TimerEvent.TIMER, sample);
			
			init();
		}
		
		/**
		 * 
		 * Specified by the IMXMLObject interface.
		 * 
		 * @param document
		 * @param id
		 * 
		 */		
		public function initialized(document:Object, id:String):void {
			parentView		= UIComponent(document);
			parentView.addEventListener(FlexEvent.CREATION_COMPLETE, parentCreated);
		}
		
		/**
		 * 
		 * Sets the line thickness of the gesture indicator line, which 
		 * shows the user the shape of the gesture they're drawing.
		 * 
		 * @param thickness How thick the gesture path line should be.
		 * 
		 * 
		 */		
		public function get lineThickness():int {
			return _lineThickness;
		}
		
		public function set lineThickness(thickness:int):void {
			_lineThickness = thickness;
		}
		
		/**
		 * 
		 * Sets the color of the gesture indicator line, which shows
		 * the user the shape of the gesture they're drawing.
		 * 
		 * @param color What color to draw the gesture indicator line.
		 * 
		 * 
		 */		
		public function get lineColor():uint {
			return _lineColor;
		}
		
		public function set lineColor(color:uint):void {
			_lineColor = color;
		}
		
		/**
		 * Should the alt key be required to draw a mouse gesture?
		 * 
		 * @param expectAlt 
		 * 
		 */		
		public function get altKey():Boolean {
			return _expectAltKey;
		}
		
		public function set altKey(expectAlt:Boolean):void {
			_expectAltKey = expectAlt;
		}
		
		/**
		 * Should the control/command key be required to draw a mouse gesture?
		 * 
		 * @param expectCtrl 
		 * 
		 */		
		public function get controlKey():Boolean {
			return _expectControlKey;
		}
		
		public function set controlKey(expectCtrl:Boolean):void {
			_expectControlKey = expectCtrl;
		}
		
		/**
		 * Should the shift key be required to draw a mouse gesture?
		 * 
		 * @param expectShift 
		 * 
		 */		
		public function get shiftKey():Boolean {
			return _expectShiftKey;
		}
		
		public function set shiftKey(expectShift:Boolean):void {
			_expectShiftKey = expectShift;
		}
		
		/**
		 * Should the space bar be required to draw a mouse gesture? (experemental)
		 * 
		 * @param expectSpace 
		 * 
		 */		
		public function get spaceKey():Boolean {
			return _expectSpaceKey;
		}
		
		public function set spaceKey(expectSpace:Boolean):void {
			_expectSpaceKey = expectSpace;
		}
		
		/**
		 * Stop drawing, throw away drawing canvas, stop tracking gesture.
		 * 
		 */		
		public function endGesture():void {
			if (drawingCanvas != null) {
				drawingCanvas.graphics.clear();
				if (sm.contains(drawingCanvas)) {
					sm.removeChild(drawingCanvas);
				}
			}
			timer.stop();
			timer.reset();
			init();
		}
		
		private function parentCreated(e:Event):void {
			sm				= parentView.systemManager;
			stage			= sm.stage;
			drawingCanvas 	= new Canvas();
			sm.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
			sm.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
			sm.addEventListener(MouseEvent.ROLL_OUT, mouseLeftStage);
			sm.addEventListener(MouseEvent.ROLL_OVER, rollOverStage);
			sm.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
			sm.addEventListener(KeyboardEvent.KEY_UP, onKeyUp);
		}
      
		private function init():void {
			mouseDown		= false;
			mouseOutOfStage	= false;
			samples			= [];
			gestures		= [];
			lastSample		= null;
			lastDirection	= null;
			inactiveSamples	= 0;
		}
		
		private function sample(te:TimerEvent):void {
			if (mouseDown) {
				var dx:Number = stage.mouseX - lastSample.x
				var dy:Number = stage.mouseY - lastSample.y
				if (dx != 0 || dy != 0) {
					drawingCanvas.graphics.moveTo(lastSample.x, lastSample.y);
					drawingCanvas.graphics.lineTo(stage.mouseX, stage.mouseY);
					lastSample = new Point(stage.mouseX, stage.mouseY)
					samples.push([dx, dy])
					analyzeSamples();
					if (GestureInterpereter.getInstance().matchesGesture(gestures)) {
						var gesture:Gesture = GestureInterpereter.getInstance().getGesture(gestures)
						dispatchEvent(new GestureRecognizedEvent(gesture));
					}
					te.updateAfterEvent();
				} else {
					inactiveSamples++
					if (inactiveSamples > inactivityLimit) {
						endGesture();
					}
				}
			}
		}
		
		private function onKeyDown(ke:KeyboardEvent):void {
			if (ke.charCode == Keyboard.SPACE) spaceDown = true;
		}
			
		private function onKeyUp(ke:KeyboardEvent):void {
			if (ke.charCode == Keyboard.SPACE) spaceDown = false;
		}
			
		private function onMouseDown(me:MouseEvent):void {
			if (correctModifiersDown(me)) {
				originalMouseEvent = me;
				mouseDown = true;
				drawingCanvas.width = stage.width;
				drawingCanvas.height = stage.height;
				drawingCanvas.graphics.lineStyle(_lineThickness, _lineColor, 1);
				lastSample = new Point(stage.mouseX, stage.mouseY);
				sm.addChild(drawingCanvas);
				timer.start();
			}
		}
			
		private function onMouseUp(me:MouseEvent):void {
			if (mouseDown) {
				var endingMouseEvent:MouseEvent = me;
				if (!GestureInterpereter.getInstance().matchesGesture(gestures)) {
					dispatchEvent(new UnrecognizedGestureEvent(gestures));
				} else {
					GestureInterpereter.getInstance().executeGesture(gestures, originalMouseEvent, endingMouseEvent); 
				}
				endGesture();
			}
		}
		
		private function mouseLeftStage(me:MouseEvent):void {
			mouseOutOfStage = true;
		}
		
		//Note, this doesn't work right now, there seems to be a bug with buttonDown after leaving and re-entering the stage
		private function rollOverStage(me:MouseEvent):void {
			if (mouseOutOfStage) { // this is the first time the mouse is over the stage after leaving the stage
				mouseOutOfStage = false;
				if (!me.buttonDown) {
					endGesture();
				}
			}
		}
		
		private function analyzeSamples():void {
			var lastDX:int
			var lastDY:int
			var currentDX:int
			var currentDY:int
			var lastDXNegative:Boolean;
			var lastDYNegative:Boolean;
			var currentDXNegative:Boolean;
			var currentDYNegative:Boolean;
			var currentDirection:String;
			
			if (samples.length > pathAnalysisResolution) {
				currentDX = samples[samples.length-1][0]
				currentDY = samples[samples.length-1][1]
				if (Math.abs(currentDX) > Math.abs(currentDY) && Math.abs(currentDY) < secondaryAxisTolerance) {
					currentDY = 0
				} else if (Math.abs(currentDY) > Math.abs(currentDX) && Math.abs(currentDX) < secondaryAxisTolerance) {
					currentDX = 0
				}
				lastDX = samples[samples.length-2][0]
				lastDY = samples[samples.length-2][1]
				currentDXNegative = (Math.abs(currentDX) != currentDX)
				currentDYNegative = (Math.abs(currentDY) != currentDY)
				lastDXNegative = (Math.abs(lastDX) != lastDX)
				lastDYNegative = (Math.abs(lastDY) != lastDY)
				if (currentDX == 0 && currentDYNegative) {
					currentDirection = Gestures.N;
					
				} else if (currentDX == 0 && !currentDYNegative) {
					currentDirection = Gestures.S;
					
				} else if (currentDY == 0 && currentDXNegative) {
					currentDirection = Gestures.W;
					
				} else if (currentDY == 0 && !currentDXNegative) {
					currentDirection = Gestures.E;
					
				} else if (currentDXNegative && currentDYNegative) {
					currentDirection = Gestures.NW;
					
				} else if (!currentDXNegative && currentDYNegative) {
					currentDirection = Gestures.NE;
					
				} else if (currentDXNegative && !currentDYNegative) {
					currentDirection = Gestures.SW;
					
				} else if (!currentDXNegative && !currentDYNegative) {
					currentDirection = Gestures.SE;
				}
				if (currentDirection != lastDirection) {
					gestures.push(currentDirection);
					lastDirection = currentDirection;
				}
			}
		}
		
		private function correctModifiersDown(me:MouseEvent):Boolean {
			var altDown:Boolean				= me.altKey;
			var shiftDown:Boolean			= me.shiftKey;
			var ctrlDown:Boolean			= me.ctrlKey;
			
			if (_expectAltKey && !altDown) return false;
			if (_expectShiftKey && !shiftDown) return false;
			if (_expectControlKey && !ctrlDown) return false;
			if (_expectSpaceKey && !spaceDown) return false;
			
			return true;
		}

	}
}